/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE
* for more details.
*
* You should have received a copy of the GNU GENERAL PUBLIC LICENSE
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.activities.graph;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.Description;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import li.klass.fhem.AndFHEMApplication;
import li.klass.fhem.R;
import li.klass.fhem.activities.core.Updateable;
import li.klass.fhem.constants.Actions;
import li.klass.fhem.constants.ResultCodes;
import li.klass.fhem.domain.core.FhemDevice;
import li.klass.fhem.service.graph.GraphEntry;
import li.klass.fhem.service.graph.gplot.GPlotDefinition;
import li.klass.fhem.service.graph.gplot.GPlotSeries;
import li.klass.fhem.service.graph.gplot.SvgGraphDefinition;
import li.klass.fhem.service.intent.DeviceIntentService;
import li.klass.fhem.service.intent.RoomListIntentService;
import li.klass.fhem.util.DisplayUtil;
import li.klass.fhem.util.FhemResultReceiver;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static li.klass.fhem.constants.Actions.DEVICE_GRAPH;
import static li.klass.fhem.constants.BundleExtraKeys.CONNECTION_ID;
import static li.klass.fhem.constants.BundleExtraKeys.DEVICE;
import static li.klass.fhem.constants.BundleExtraKeys.DEVICE_GRAPH_DEFINITION;
import static li.klass.fhem.constants.BundleExtraKeys.DEVICE_GRAPH_ENTRY_MAP;
import static li.klass.fhem.constants.BundleExtraKeys.DEVICE_NAME;
import static li.klass.fhem.constants.BundleExtraKeys.DO_REFRESH;
import static li.klass.fhem.constants.BundleExtraKeys.END_DATE;
import static li.klass.fhem.constants.BundleExtraKeys.RESULT_RECEIVER;
import static li.klass.fhem.constants.BundleExtraKeys.START_DATE;
import static li.klass.fhem.util.DateFormatUtil.ANDFHEM_DATE_FORMAT;
public class ChartingActivity extends AppCompatActivity implements Updateable {
private static final int REQUEST_TIME_CHANGE = 1;
private static final int DIALOG_EXECUTING = 2;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
private String deviceName;
private SvgGraphDefinition svgGraphDefinition;
private DateTime startDate;
private DateTime endDate;
private String connectionId;
/**
* Jumps to the charting activity.
*
* @param context calling intent
* @param device concerned device
* @param connectionId connection ID
* @param graphDefinition series descriptions each representing one series in the resulting chart
*/
@SuppressWarnings("unchecked")
public static void showChart(Context context, FhemDevice device, String connectionId, SvgGraphDefinition graphDefinition) {
context.startActivity(new Intent(context, ChartingActivity.class)
.putExtra(DEVICE_NAME, device.getName())
.putExtra(CONNECTION_ID, connectionId)
.putExtra(DEVICE_GRAPH_DEFINITION, graphDefinition));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chart);
((AndFHEMApplication) getApplication()).getDaggerComponent().inject(this);
if (savedInstanceState != null && savedInstanceState.containsKey(START_DATE)) {
startDate = (DateTime) savedInstanceState.getSerializable(START_DATE);
}
if (savedInstanceState != null && savedInstanceState.containsKey(END_DATE)) {
endDate = (DateTime) savedInstanceState.getSerializable(END_DATE);
}
Bundle extras = getIntent().getExtras();
deviceName = extras.getString(DEVICE_NAME);
connectionId = extras.getString(CONNECTION_ID);
svgGraphDefinition = (SvgGraphDefinition) extras.getSerializable(DEVICE_GRAPH_DEFINITION);
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
update(false);
}
@Override
public void update(final boolean doUpdate) {
startService(new Intent(Actions.GET_DEVICE_FOR_NAME)
.setClass(this, RoomListIntentService.class)
.putExtra(DEVICE_NAME, deviceName)
.putExtra(CONNECTION_ID, connectionId)
.putExtra(DO_REFRESH, doUpdate)
.putExtra(RESULT_RECEIVER, new FhemResultReceiver() {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode != ResultCodes.SUCCESS) return;
readDataAndCreateChart(doUpdate, (FhemDevice) resultData.getSerializable(DEVICE));
}
}));
}
/**
* Reads all the charting data for a given date and the column specifications set as attribute.
*
* @param doRefresh should the underlying room device list be refreshed?
* @param device concerned device
*/
@SuppressWarnings("unchecked")
private void readDataAndCreateChart(boolean doRefresh, final FhemDevice device) {
showDialog(DIALOG_EXECUTING);
startService(new Intent(DEVICE_GRAPH)
.setClass(this, DeviceIntentService.class)
.putExtra(DO_REFRESH, doRefresh)
.putExtra(DEVICE_NAME, deviceName)
.putExtra(START_DATE, startDate)
.putExtra(END_DATE, endDate)
.putExtra(CONNECTION_ID, connectionId)
.putExtra(DEVICE_GRAPH_DEFINITION, svgGraphDefinition)
.putExtra(RESULT_RECEIVER, new FhemResultReceiver() {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == ResultCodes.SUCCESS) {
startDate = (DateTime) resultData.get(START_DATE);
endDate = (DateTime) resultData.get(END_DATE);
createChart(device, (Map<GPlotSeries, List<GraphEntry>>) resultData.get(DEVICE_GRAPH_ENTRY_MAP));
}
try {
dismissDialog(DIALOG_EXECUTING);
} catch (Exception e) {
Log.e(ChartingActivity.class.getName(), "error while hiding dialog", e);
}
}
}));
}
/**
* Actually creates the charting view by using the newly read charting data.
*
* @param device concerned device
* @param graphData used graph data
*/
@SuppressWarnings("unchecked")
private void createChart(FhemDevice device, Map<GPlotSeries, List<GraphEntry>> graphData) {
handleDiscreteValues(graphData);
LineData lineData = createLineDataFor(graphData);
String title;
if (DisplayUtil.getWidthInDP() < 500) {
title = device.getAliasOrName() + "\n\r" +
DATE_TIME_FORMATTER.print(startDate) + " - " + DATE_TIME_FORMATTER.print(endDate);
} else {
title = device.getAliasOrName() + " " +
DATE_TIME_FORMATTER.print(startDate) + " - " + DATE_TIME_FORMATTER.print(endDate);
}
getSupportActionBar().setTitle(title);
LineChart lineChart = (LineChart) findViewById(R.id.chart);
// must be called before setting chart data!
GPlotDefinition plotDefinition = svgGraphDefinition.getPlotDefinition();
setRangeFor(plotDefinition.getLeftAxis().getRange(), lineChart.getAxisLeft());
setRangeFor(plotDefinition.getRightAxis().getRange(), lineChart.getAxisRight());
XAxis xAxis = lineChart.getXAxis();
xAxis.setValueFormatter(new IAxisValueFormatter() {
@Override
public String getFormattedValue(float value, AxisBase axis) {
return ANDFHEM_DATE_FORMAT.print((long) value);
}
});
xAxis.setLabelRotationAngle(300);
int labelCount = DisplayUtil.getWidthInDP() / 150;
xAxis.setLabelCount(labelCount < 2 ? 2 : labelCount, true);
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
Description description = new Description();
description.setText("");
lineChart.setDescription(description);
lineChart.setNoDataText(getString(R.string.noGraphEntries));
lineChart.setData(lineData);
lineChart.setMarkerView(new ChartMarkerView(this));
lineChart.animateX(200);
}
private void setRangeFor(Optional<Range<Double>> axisRange, com.github.mikephil.charting.components.YAxis axis) {
if (axisRange.isPresent()) {
Range<Double> range = axisRange.get();
if (range.hasLowerBound()) {
axis.setAxisMinimum(range.lowerEndpoint().floatValue());
}
if (range.hasUpperBound()) {
axis.setAxisMaximum(range.upperEndpoint().floatValue());
}
}
}
private void handleDiscreteValues(Map<GPlotSeries, List<GraphEntry>> graphData) {
for (Map.Entry<GPlotSeries, List<GraphEntry>> entry : graphData.entrySet()) {
if (!isDiscreteSeries(entry.getKey())) {
continue;
}
float previousValue = -1;
List<GraphEntry> newData = newArrayList();
List<GraphEntry> values = entry.getValue();
for (GraphEntry graphEntry : values) {
DateTime date = graphEntry.getDate();
float value = graphEntry.getValue();
if (previousValue == -1) {
previousValue = value;
}
newData.add(new GraphEntry(date.minusMillis(1), previousValue));
newData.add(new GraphEntry(date, value));
newData.add(new GraphEntry(date.plusMillis(1), value));
previousValue = value;
}
values.clear();
values.addAll(newData);
}
}
private boolean isDiscreteSeries(GPlotSeries plotSeries) {
GPlotSeries.LineType lineType = plotSeries.getLineType();
return lineType == GPlotSeries.LineType.STEPS || lineType == GPlotSeries.LineType.FSTEPS || lineType == GPlotSeries.LineType.HISTEPS;
}
private LineData createLineDataFor(Map<GPlotSeries, List<GraphEntry>> graphData) {
ImmutableList<ILineDataSet> lineDataItems = from(graphData.entrySet())
.transform(new Function<Map.Entry<GPlotSeries, List<GraphEntry>>, ILineDataSet>() {
@Override
public ILineDataSet apply(Map.Entry<GPlotSeries, List<GraphEntry>> input) {
return lineDataSetFrom(input);
}
})
.toList();
return new LineData(lineDataItems);
}
private ILineDataSet lineDataSetFrom(Map.Entry<GPlotSeries, List<GraphEntry>> entry) {
GPlotSeries series = entry.getKey();
ImmutableList<Entry> yEntries = from(entry.getValue()).transform(new Function<GraphEntry, Entry>() {
@Override
public Entry apply(GraphEntry input) {
assert input != null;
return new Entry(input.getDate().getMillis(), input.getValue());
}
}).toList();
LineDataSet lineDataSet = new LineDataSet(yEntries, series.getTitle());
lineDataSet.setAxisDependency(series.getAxis() == GPlotSeries.Axis.LEFT ?
YAxis.AxisDependency.LEFT :
YAxis.AxisDependency.RIGHT);
lineDataSet.setColor(series.getColor().getHexColor());
lineDataSet.setCircleColor(series.getColor().getHexColor());
lineDataSet.setFillColor(series.getColor().getHexColor());
lineDataSet.setDrawCircles(false);
lineDataSet.setDrawValues(false);
lineDataSet.setLineWidth(series.getLineWidth());
switch (series.getSeriesType()) {
case FILL:
lineDataSet.setDrawFilled(true);
break;
case DOT:
lineDataSet.enableDashedLine(3, 2, 1);
break;
}
switch (series.getLineType()) {
case POINTS:
lineDataSet.enableDashedLine(3, 2, 1);
break;
}
if (isDiscreteSeries(series)) {
lineDataSet.setMode(LineDataSet.Mode.STEPPED);
}
return lineDataSet;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.graph_menu, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
case R.id.menu_changeStartEndDate:
startActivityForResult(new Intent(this, ChartingDateSelectionActivity.class)
.putExtra(DEVICE_NAME, deviceName).putExtra(START_DATE, startDate)
.putExtra(END_DATE, endDate), REQUEST_TIME_CHANGE);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) {
super.onActivityResult(requestCode, resultCode, resultIntent);
if (resultIntent != null && resultCode == RESULT_OK) {
Bundle bundle = resultIntent.getExtras();
switch (requestCode) {
case REQUEST_TIME_CHANGE:
startDate = (DateTime) bundle.getSerializable(START_DATE);
endDate = (DateTime) bundle.getSerializable(END_DATE);
update(false);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(START_DATE, startDate);
outState.putSerializable(END_DATE, endDate);
}
@Override
protected Dialog onCreateDialog(int id) {
super.onCreateDialog(id);
switch (id) {
case DIALOG_EXECUTING:
return ProgressDialog.show(this, "", getResources().getString(R.string.executing));
}
return null;
}
}